package md.frolov.bible.initializers;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

import md.frolov.bible.Constants;
import md.frolov.bible.indexes.IX;
import md.frolov.bible.indexes.IndexManager;
import md.frolov.bible.model.Verse;

public class CustomXMLParser {
	private File file;
	private TagStack tagStack = new TagStack();
	private Reader fis;
	private Attributes attributes = new Attributes();
	private ParsingHandler handler;
	private MyStringBuilder tagBuf = new MyStringBuilder(50);
	private MyStringBuilder attrKeyBuf = new MyStringBuilder(50);
	private MyStringBuilder attrValBuf = new MyStringBuilder(100);
	private MyStringBuilder textData = new MyStringBuilder(1024);
	private boolean handleTextData = false;
	private final static int BUFSIZE = 8*1024; 
	private char[] buffer = new char[BUFSIZE];
	private long offset;
	
//	long lastTime = 0;
//	int tagCnt = 0;
//	int attrCnt = 0;
	
	/*private void checkPoint(String str){
		long time = System.currentTimeMillis();
		if(lastTime == 0){
			lastTime = time;
			System.out.print("[started] ");
			System.out.println(str);
			return;
		}
		System.out.print("[checkpoint] ");
		System.out.print(str);
		System.out.print(" (");
		System.out.print(tagCnt);
		System.out.print(",");
		System.out.print(attrCnt);
		System.out.print(",");
		System.out.print(offset);
		System.out.print("): ");
		System.out.println(time-lastTime);
		lastTime = time;
		attrCnt = 0;
		tagCnt = 0;
	} */
	
	public CustomXMLParser(File file){
		this.file = file;
	}
	
	public void parse(ParsingHandler myHandler) throws IOException{
		parse(myHandler,false);
	}
	
	public void parse(ParsingHandler myHandler, boolean handleTextData) throws IOException{
		this.handleTextData = handleTextData;
//		mbr = new BufferedReaderWithOffset(new InputStreamReader(new FileInputStream(file), Constants.ENCODING));
//		checkPoint("parsing started");
		fis = new InputStreamReader(new FileInputStream(file), Constants.ENCODING);
	
//		checkPoint("FIS created");
		this.handler = myHandler;
		
		try{
//			checkPoint("before scan");
			scan();
		} catch(ParserException e){
			System.out.println("parser exception");
			e.printStackTrace();
			handler.error();
		} catch(EOFException eof){
			System.out.println("EOF");
		} finally {
			fis.close();
		}

//		fis = new InputStreamReader(new FileInputStream(file), Constants.ENCODING);
//		fis.skip(offset);
//		char[] buf = new char[30];
//		fis.read(buf);
//		System.out.println("Read symbol last: "+new String(buf));
//		System.out.println("stack:");
//		while(!tagStack.isEmpty()){
//			System.out.println(tagStack.pop());
//		}
//		System.out.println();
	}
	
	private void scan() throws IOException, ParserException{
		int len;
		int i;
		offset=0;
		
		char c;
		
		boolean beginTag = false;
		boolean endTag = false;
		
		boolean beginHandleTextData = false;
		
		final int PHASE_CHECK_HEADER = 1;
		final int PHASE_FIND_NEXT_TAG = 2;
		final int PHASE_BEGIN_TAG = 3;
		final int PHASE_BEGIN_ATTRIBUTES = 4;
		int phase = PHASE_CHECK_HEADER;
		
		long tagOffset = 0;
		
		boolean isEndTag = false;
		boolean isBoth = false;
		
		boolean isValue = false;
		boolean isKey = false;
		boolean isEqual = false;
		
		while((len=fis.read(buffer))!=-1){
			for(i=0;i<len;i++,offset++){
				c = buffer[i];
				if(PHASE_FIND_NEXT_TAG == phase){
					if(Character.isWhitespace(c) && !beginHandleTextData) continue;
					if(c=='<'){
						if(handleTextData && beginHandleTextData){
							handler.textData(textData.toString());
							beginHandleTextData = false;
						}
						tagOffset = offset;
						phase = PHASE_BEGIN_TAG;
						attributes.clear();
						tagBuf.clear();
//						tagCnt++;
						continue;
					}
					if(handleTextData){
						beginHandleTextData = true;
						textData.appendCodePoint(c);
					}
					continue;
				}

				if(PHASE_BEGIN_TAG == phase){
					if(Character.isUnicodeIdentifierPart(c)){
						tagBuf.appendCodePoint(c);
						continue;
					}
					if(Character.isWhitespace(c)) {
						if(isEndTag) throw new ParserException();
						phase = PHASE_BEGIN_ATTRIBUTES;
						attributes.clear();
						continue;
					}
					
					if(c=='/'){
						if(isEndTag) throw new ParserException();
						isEndTag = true;
						if(tagBuf.length()>0) isBoth = true;
						continue;
					}
					
					if(c=='>') {
						if(isEndTag){
							if(isBoth){
								startElement(tagOffset);
								isBoth = false;
							}
							endElement();
							isEndTag = false;
						} else {
							startElement(tagOffset);
						}
						phase = PHASE_FIND_NEXT_TAG;
						continue;
					}
					throw new ParserException();
				}
				
				if(PHASE_BEGIN_ATTRIBUTES == phase){
					if(c=='"'){
						if(isValue){
							isValue=false;
							attributes.put(attrKeyBuf.toString(), attrValBuf.toString());
							attrKeyBuf.clear();
							attrValBuf.clear();
//							attrCnt++;
							continue;
						}
						if(isEqual){
							isValue = true;
							isEqual = false;
							continue;
						}
					}
					
					if(isValue){
						attrValBuf.appendCodePoint(c);
						continue;
					}
					
					if(isEndTag && c!='>') throw new ParserException();
					
					if(Character.isWhitespace(c)){
						isKey = false;
						continue;
					}
					
					if(c=='='){
						isKey = false;
						isEqual = true;
						continue;
					}
					
					if(Character.isUnicodeIdentifierPart(c) && !isEqual){
						if(!isKey && attrKeyBuf.length()>0) throw new ParserException();
						isKey = true;
						attrKeyBuf.appendCodePoint(c);
						continue;
					}
					
					if(c=='/'){
						isEndTag = true;
						continue;
					}
					
					if(c=='>'){
						if(attrKeyBuf.length()>0 || attrValBuf.length()>0) throw new ParserException();
						startElement(tagOffset);
						if(isEndTag) {
							endElement();
							isEndTag = false;
						}
						phase = PHASE_FIND_NEXT_TAG;
						continue;
					}
					
					throw new ParserException();
				}
				
				if(PHASE_CHECK_HEADER == phase){
					if(Character.isWhitespace(c) && !beginTag) continue;
					
					if(c=='<' && !beginTag){
						beginTag = true;
						tagBuf.clear();
						continue;
					}
					
					if(beginTag && !Character.isWhitespace(c)){
						tagBuf.appendCodePoint(c);
						if(tagBuf.length() == "?xml".length())
							if(tagBuf.toString().equals("?xml")){
								beginTag = false;
								continue;
							} else {
								throw new ParserException();
							}
								
					}
					
					if(beginTag && Character.isWhitespace(c)){
						beginTag = false;
						continue;
					}
					
					if(tagBuf.length()>0 && c=='?'){
						endTag = true;
						continue;
					}
					
					if(endTag && c=='>'){
						phase = PHASE_FIND_NEXT_TAG;
						endTag = false;
//						checkPoint("end of header");
						continue;
					}
					
					continue;
//					throw new ParserException();
				}
				
				throw new ParserException();
			}
		}
	}
	
	private void startElement(long tagOffset) throws ParserException{
		if(tagBuf.length()==0) throw new ParserException();
		String tag = tagBuf.toString();
		tagStack.push(tag);
		handler.startElement(tag, attributes, tagOffset);
	}
	
	private void endElement() throws ParserException, EOFException{
		if(tagBuf.length()==0) throw new ParserException();
		String tag = tagBuf.toString();
		if(!tag.equals(tagStack.pop())) throw new ParserException();
		handler.endElement(tag);
		if(tagStack.isEmpty()) 
			throw new EOFException();
	}
	
	public Verse[] getChapter(String chapterId) throws IOException, ParserException{
		List<Verse> verses = new ArrayList<Verse>();
		
		offset = IndexManager.getInstance().getOffset(file, chapterId);
		
		openFile();
		fis.skip(offset);
		
		int len;
		char c;
		int i;
	
		boolean isEndTag = false;
		boolean isBoth = false;
		boolean isValue = false;
		boolean isKey = false;
		boolean isEqual = false;
		boolean beginHandleTextData = false;
		boolean beginChapter = false;
		
		handleTextData = false;
		textData.clear();
		
		final int PHASE_FIND_NEXT_TAG = 1;
		final int PHASE_BEGIN_TAG = 2;
		final int PHASE_BEGIN_ATTRIBUTES = 3;
		int phase = PHASE_FIND_NEXT_TAG;
		
		String key = null;
		String value = null;
		
		String vId = null;
		String vContents = null;
		String vLocalId = null;
		
		Verse verse = null;

		mainloop:
		while((len=fis.read(buffer))!=-1){
			for(i=0;i<len;i++){
				c = buffer[i];
				
				if(PHASE_FIND_NEXT_TAG == phase){
					if(Character.isWhitespace(c) && !beginHandleTextData) continue;
					if(c!='<' && !beginChapter) continue;
					if(c=='<'){
						if(handleTextData && beginHandleTextData){
							vContents = textData.toString().trim();
							textData.clear();
							beginHandleTextData = false;
						}
						phase = PHASE_BEGIN_TAG;
						tagBuf.clear();
						continue;
					}
					if(handleTextData){
						beginHandleTextData = true;
						textData.appendCodePoint(c);
					}
					continue;
				} //endof PHASE_FIND_NEXT_TAG

				if(PHASE_BEGIN_TAG == phase){
					if(Character.isUnicodeIdentifierPart(c)){
						tagBuf.appendCodePoint(c);
						continue;
					}
					if(Character.isWhitespace(c)) {
						if(isEndTag) throw new ParserException();
						if(!beginChapter)
							if(!tagBuf.toString().equals(Constants.XML_CHAPTER_TAG)){
								phase = PHASE_FIND_NEXT_TAG;
								continue;
							}
						phase = PHASE_BEGIN_ATTRIBUTES;
						continue;
					}
					
					if(c=='/'){
						if(isEndTag) throw new ParserException();
						isEndTag = true;
						if(tagBuf.length()>0) isBoth = true;
						continue;
					}
					
					if(c=='>') {
						if(beginChapter && isEndTag && !isBoth && vContents!=null){
							verse = Utils.getVerse(file, vId, vLocalId, chapterId, vContents, false);
							verses.add(verse);
							
							vId = null;
							vLocalId = null;
							vContents = null;
							handleTextData = false;
						}
						
						if(beginChapter && isEndTag)
							if(tagBuf.toString().equals(Constants.XML_CHAPTER_TAG)){
								break mainloop;
							}
						
						isEndTag = false;
						isBoth = false;
						phase = PHASE_FIND_NEXT_TAG;
						continue;
					}
					throw new ParserException();
				} //endof PHASE_BEGIN_TAG
				
				if(PHASE_BEGIN_ATTRIBUTES == phase){
					if(c=='"'){
						if(isValue){
							isValue=false;
							key = attrKeyBuf.toString();
							value = attrValBuf.toString();
							attrKeyBuf.clear();
							attrValBuf.clear();
							if(key.equals(Constants.XML_ATTRIBUTE_ID)){
								vId = value;
								
								if(!beginChapter){
									if(vId.equals(chapterId)){
										beginChapter = true;
										continue;
									} else {
										phase = PHASE_FIND_NEXT_TAG;
										continue;
									}
								}
							}
							
							if(key.equals(Constants.XML_ATTRIBUTE_VERSE_LOCAL_ID)){
								vLocalId = value;
							}
							
							continue;
						}
						if(isEqual){
							isValue = true;
							isEqual = false;
							continue;
						}
					}
					
					if(isValue){
						attrValBuf.appendCodePoint(c);
						continue;
					}
					
					if(isEndTag && c!='>') throw new ParserException();
					
					if(Character.isWhitespace(c)){
						isKey = false;
						continue;
					}
					
					if(c=='='){
						isKey = false;
						isEqual = true;
						continue;
					}
					
					if(Character.isUnicodeIdentifierPart(c) && !isEqual){
						if(!isKey && attrKeyBuf.length()>0) throw new ParserException();
						isKey = true;
						attrKeyBuf.appendCodePoint(c);
						continue;
					}
					
					if(c=='/'){
						isEndTag = true;
						continue;
					}
					
					if(c=='>'){
						if(attrKeyBuf.length()>0 || attrValBuf.length()>0) throw new ParserException();
						handleTextData = !isEndTag && tagBuf.toString().equals(Constants.XML_VERSE_TAG);
						isEndTag = false;
						phase = PHASE_FIND_NEXT_TAG;
						continue;
					}
					
					throw new ParserException();
				} //endof PHASE_BEGIN_ATTRIBUTES
				
			}
		}
		
		closeFile();
		Verse[] result = new Verse[verses.size()];
		for(i=0;i<verses.size();i++)
			result[i] = verses.get(i);
		return result;
	}
	
	public Verse[] getVerses(String[] ids) throws IOException, ParserException{
		IX ix = IndexManager.getInstance().getIndex(file);
		
		long[] offsets = new long[ids.length];
		for(int i=0;i<ids.length;i++)
			offsets[i] = ix.getOffset(ids[i]);

		//sorting offsets
		long l;
		String s;
		for(int i=0;i<ids.length;i++)
			for(int j=i;j<ids.length;j++){
				if(offsets[j]<offsets[i]){
					l = offsets[j];
					s = ids[j];
					offsets[j] = offsets[i];
					ids[j] = ids[i];
					offsets[i] = l;
					ids[i] = s;
				}
			}
		
		Verse[] result = new Verse[ids.length];

		//beginning to search
		offset = 0;
		int curOffsetIx = 0;
		long charsToSkip;
		int len;
		int i;
		int j;
		char c;
		
		boolean isEndTag = false;
		boolean isBoth = false;
		boolean isValue = false;
		boolean isKey = false;
		boolean isEqual = false;
		boolean beginHandleTextData = false;
		boolean skipping = false;
		boolean collectingComplexVerse = false;
		
		handleTextData = false;
		textData.clear();
		
		final int PHASE_FIND_NEXT_TAG = 1;
		final int PHASE_BEGIN_TAG = 2;
		final int PHASE_BEGIN_ATTRIBUTES = 3;
		int phase = PHASE_FIND_NEXT_TAG;
		
		openFile();
		
		String key = null;
		String value = null;
		
		String vId = null;
		String vContents = null;
		String vLocalId = null;
		
		Verse verse = null;
		
		String tag = null;
		boolean isChapter = false;
		String lastChapterId = null;
		
		mainLoop:
		while(true){
			//get char cnt to skip
			charsToSkip = offsets[curOffsetIx] - offset;
			if(charsToSkip>0){
				offset+=charsToSkip;
				fis.skip(charsToSkip);
				lastChapterId = null;
			} else {
				boolean cont = false;
				for(j=curOffsetIx;j<offsets.length;j++){
					if(offset-offsets[j]>BUFSIZE*2){
						curOffsetIx = j+1;
						cont = true;
					}
				}
				if(curOffsetIx>=offsets.length) break;
				if(cont)continue;
			}
			
			len=fis.read(buffer);
			if(len==-1) break;
			
			charloop:
			for(i=0;i<len;i++){
				c = buffer[i];
				if(PHASE_FIND_NEXT_TAG == phase){
					if(Character.isWhitespace(c) && !beginHandleTextData) continue;
					if(c!='<' && skipping) continue;
					if(c=='<'){
						if(handleTextData && beginHandleTextData){
							if(collectingComplexVerse){
								textData.appendCodePoint(' ');
							} else {
								vContents = textData.toString().trim();
								textData.clear();
							}
							beginHandleTextData = false;
						}
						skipping = false;
						phase = PHASE_BEGIN_TAG;
						tagBuf.clear();
						continue;
					}
					if(handleTextData){
						beginHandleTextData = true;
						textData.appendCodePoint(c);
					}
					continue;
				}

				if(PHASE_BEGIN_TAG == phase){
					if(Character.isUnicodeIdentifierPart(c)){
						tagBuf.appendCodePoint(c);
						continue;
					}
					if(Character.isWhitespace(c)) {
						if(isEndTag) throw new ParserException();
						tag = tagBuf.toString();
						if(tag.equals(Constants.XML_CHAPTER_TAG)){
							phase = PHASE_BEGIN_ATTRIBUTES;
							isChapter = true;
							continue;
						}
						if(tag.equals(Constants.XML_VERSE_TAG)){
							phase = PHASE_BEGIN_ATTRIBUTES;
							continue;
						}
						phase = PHASE_FIND_NEXT_TAG;
						skipping = true;
						continue;
					}
					
					if(c=='/'){
						if(isEndTag) throw new ParserException();
						isEndTag = true;
						if(tagBuf.length()>0) isBoth = true;
						continue;
					}
					
					if(c=='>') {
						if(!tagBuf.toString().equals(Constants.XML_VERSE_TAG)){
							isEndTag = false;
							phase = PHASE_FIND_NEXT_TAG;
							skipping = true;
							continue;
						}
						if(isEndTag && !isBoth && (vContents!=null || collectingComplexVerse)){
							for(j=curOffsetIx;j<offsets.length && offsets[j]==offsets[curOffsetIx];j++){
								if(ids[j].equals(vId)){
									verse = Utils.getVerse(file,vId, vLocalId, lastChapterId, vContents, true);
									result[j] = verse;
									continue;
								}
								if(ids[j].startsWith(vId+"-")){
									collectingComplexVerse = true;
									phase = PHASE_FIND_NEXT_TAG;
									skipping = true;
									isEndTag = false;
									continue charloop;
								}
								
								if(ids[j].endsWith("-"+vId)){
									verse = Utils.getVerse(file,ids[j], "", lastChapterId, textData.toString(), true);
									result[j] = verse;
									collectingComplexVerse = false;
									textData.clear();
								}
							}
							
							vId = null;
							vLocalId = null;
							vContents = null;
							handleTextData = false;
							
							for(j=curOffsetIx;j<offsets.length;j++){
								if(result[j]==null) break;
							}
							
							if(j==offsets.length){
								break mainLoop;
							}
							
							//skipping logics
							if(offsets[j]!=offsets[curOffsetIx]){
								curOffsetIx = j;
								if(offsets[j]>=offset+len){
									i = len;
								} else {
									i = (int)(offsets[j]-offset-1);
								}
								
							}
						}
						isEndTag = false;
						isBoth = false;
						skipping = true;
						phase = PHASE_FIND_NEXT_TAG;
						continue;
					}
					throw new ParserException();
				}
				
				if(PHASE_BEGIN_ATTRIBUTES == phase){
					if(c=='"'){
						if(isValue){
							isValue=false;
							key = attrKeyBuf.toString();
							value = attrValBuf.toString();
							attrKeyBuf.clear();
							attrValBuf.clear();
							if(key.equals(Constants.XML_ATTRIBUTE_ID)){
								if(isChapter){
									lastChapterId = value;
									phase = PHASE_FIND_NEXT_TAG;
									skipping = true;
									isChapter = false;
									continue;
								}
								
								vId = value;
								for(j=curOffsetIx;j<offsets.length && offsets[j]==offsets[curOffsetIx];j++){
									if(ids[j].equals(vId)){
										handleTextData = true;
										break;
									}
									if(ids[j].length()-vId.length()>5)
										if(ids[j].startsWith(vId+"-")){
											handleTextData = true;
											collectingComplexVerse = true;
											break;
										}
								}
								if(handleTextData || collectingComplexVerse)
									continue;
								
								//not that id
								vId = null;
								vLocalId = null;
								vContents = null;
								skipping = true;
								phase = PHASE_FIND_NEXT_TAG;
								continue;
							}
							
							if(key.equals(Constants.XML_ATTRIBUTE_VERSE_LOCAL_ID)){
								vLocalId = value;
							}
							
							continue;
						}
						if(isEqual){
							isValue = true;
							isEqual = false;
							continue;
						}
					}
					
					if(isValue){
						attrValBuf.appendCodePoint(c);
						continue;
					}
					
					if(isEndTag && c!='>') throw new ParserException();
					
					if(Character.isWhitespace(c)){
						isKey = false;
						continue;
					}
					
					if(c=='='){
						isKey = false;
						isEqual = true;
						continue;
					}
					
					if(Character.isUnicodeIdentifierPart(c) && !isEqual){
						if(!isKey && attrKeyBuf.length()>0) throw new ParserException();
						isKey = true;
						attrKeyBuf.appendCodePoint(c);
						continue;
					}
					
					if(c=='/'){
						isEndTag = true;
						continue;
					}
					
					if(c=='>'){
						if(attrKeyBuf.length()>0 || attrValBuf.length()>0) throw new ParserException();
						handleTextData = true;
						if(isEndTag) {
							isEndTag = false;
						}
						phase = PHASE_FIND_NEXT_TAG;
						continue;
					}
					
					throw new ParserException();
				}
				
				throw new ParserException();
			}
			
			offset+=len;
		}
		
		closeFile();
		
		for(i=0;i<result.length;i++)
			if(result[i]==null){
				result[i] = Utils.getVerse(file,ids[i], "0", ids[i].substring(0,ids[i].lastIndexOf('.')), "<<null>> "+ids[i]+" not found", false);
			}
		
		return result;
	}
	
	private void openFile() throws IOException{
		fis = new InputStreamReader(new FileInputStream(file),Constants.ENCODING);
		//TODO check last access  time
	}
	
	private void closeFile() throws IOException{
		fis.close();
	}
}
